package de.stefanocke.roo.wicket.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.ReferenceStrategy;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.addon.plural.PluralMetadata;
import org.springframework.roo.classpath.PhysicalTypeCategory;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.TypeLocationService;
import org.springframework.roo.classpath.details.BeanInfoUtils;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetails;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.MethodMetadata;
import org.springframework.roo.classpath.scanner.MemberDetails;
import org.springframework.roo.classpath.scanner.MemberDetailsScanner;
import org.springframework.roo.metadata.MetadataDependencyRegistry;
import org.springframework.roo.metadata.MetadataIdentificationUtils;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.process.manager.ProcessManager;
import org.springframework.roo.project.LogicalPath;
import org.springframework.roo.support.logging.HandlerUtils;

@Component
@Service
@Reference(name = "propertyFilter", strategy = ReferenceStrategy.EVENT, policy = ReferencePolicy.DYNAMIC, referenceInterface = IPropertyFilter.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
public class ModelClassServiceImpl implements ModelClassService {

	private static final Logger logger =
	HandlerUtils.getLogger(ModelClassServiceImpl.class);
	static{
		logger.setLevel(Level.FINE);
	}
	
	@Reference
	private MemberDetailsScanner memberDetailsScanner;
	@Reference
	private MetadataDependencyRegistry metadataDependencyRegistry;
	@Reference
	private MetadataService metadataService;

	@Reference
	private TypeLocationService typeLocationService;

	@Reference
	private ProcessManager processManager;

	// Mutex
	private final Object lock = new Object();

	private Set<IPropertyFilter> propertyFilters = new HashSet<IPropertyFilter>();

	protected void bindPropertyFilter(final IPropertyFilter filter) {
		synchronized (lock) {
			propertyFilters.add(filter);
		}
	}

	protected void unbindPropertyFilter(final IPropertyFilter filter) {
		synchronized (lock) {
			propertyFilters.remove(filter);
		}
	}

	private boolean isGUIProperty(String clientMid, JavaType modelClass, MethodMetadata accessor) {
		for (IPropertyFilter filter : propertyFilters) {
			if (!filter.isGUIProperty(clientMid, modelClass, accessor)) {
				return false;
			}

		}
		return true;

	}

	/**
	 * {@inheritDoc}
	 * 
	 * This implementation gives all fields, that have getters and setters and
	 * which are no persistence identifiers or version fields of an entity bean.
	 */
	@Override
	public List<BeanPropertyMetadata> getScaffoldEligibleProperties(JavaType modelClass,
			String metadataIdentificationString) {
		if(processManager.isDevelopmentMode()){
			logger.fine("getScaffoldEligibleProperties for "+modelClass+". Client MID: "+metadataIdentificationString);
		}
		Validate.notNull(modelClass, "Java type required");
		Validate.notNull(metadataService, "Metadata service required");

		MemberDetails memberDetails = getMemberDetails(modelClass, metadataIdentificationString);

		Map<JavaSymbolName, BeanPropertyMetadata> properties = new LinkedHashMap<JavaSymbolName, BeanPropertyMetadata>();
		for (MethodMetadata method : memberDetails.getMethods()) {
			// Only interested in accessors
			if (!BeanInfoUtils.isAccessorMethod(method)
					|| !isGUIProperty(metadataIdentificationString, modelClass, method)) {
				continue;
			}
			BeanPropertyMetadata pm = new BeanPropertyMetadata();

			JavaSymbolName propertyName = BeanInfoUtils.getPropertyNameForJavaBeanMethod(method);
			pm.setPropertyName(new JavaSymbolName(StringUtils.uncapitalize(propertyName.getSymbolName())));

			FieldMetadata field = BeanInfoUtils.getFieldForPropertyName(memberDetails, propertyName);
			pm.setField(field);

			// Not interested in immutable fields (TODO: This is okay for forms.
			// But what about read-only views?)
			if (field == null || !BeanInfoUtils.hasAccessorAndMutator(field, memberDetails)) {
				continue;
			}

			// Register a dependency to the metadata which declares the getter
			// method
			registerDependency(method.getDeclaredByMetadataId(), metadataIdentificationString);

			JavaType type = method.getReturnType();

			if (type.isCommonCollectionType()) {
				pm.setCollection(true);
				if (type.getParameters().size() == 1) {
					// We do not consider Maps, yet
					JavaType genericType = type.getParameters().get(0);
					pm.setType(genericType);
					// setTypeDetails(metadataIdentificationString, pm,
					// genericType);
				} else {
					continue;
				}

			} else {
				pm.setType(type);
				// setTypeDetails(metadataIdentificationString, pm, type);
			}

			if (!properties.containsKey(propertyName)) {

				properties.put(propertyName, pm);
			}
		}
		return Collections.unmodifiableList(new ArrayList<BeanPropertyMetadata>(properties.values()));
	}

	

	@Override
	public JavaTypeMetadataDetails getJavaTypeMetadataDetails(String metadataIdentificationString, JavaType type) {
		
		Validate.notNull(type, "Java type required");

		JavaTypeMetadataDetails javaTypeMetadataDetails = null;
		if (typeLocationService.isInProject(type)) {
//			if(processManager.isDevelopmentMode()){
//				logger.fine("getJavaTypeMetadataDetails for "+type+". Client MID: "+metadataIdentificationString);
//			}
			ClassOrInterfaceTypeDetails cid = getClassOrInterfaceTypeDetails(type, metadataIdentificationString);
			
			javaTypeMetadataDetails = new JavaTypeMetadataDetails(type, cid, getPlural(type,
					metadataIdentificationString), isEnumType(type), typeLocationService.isInProject(type));
		}
		return javaTypeMetadataDetails;
	}

	@Override
	public MemberDetails getMemberDetails(JavaType modelClass, String clientMid) {
		ClassOrInterfaceTypeDetails classOrInterfaceDetails = getClassOrInterfaceTypeDetails(modelClass, clientMid);
		return getMemberDetails(classOrInterfaceDetails);
	}
	
	private ClassOrInterfaceTypeDetails getClassOrInterfaceTypeDetails(JavaType javaType, String clientMid) {
		ClassOrInterfaceTypeDetails classOrInterfaceDetails = typeLocationService.getTypeDetails(javaType);
		registerDependency(classOrInterfaceDetails.getDeclaredByMetadataId(), clientMid);
		return classOrInterfaceDetails;
	}

	private MemberDetails getMemberDetails(ClassOrInterfaceTypeDetails classOrInterfaceDetails) {
		return memberDetailsScanner.getMemberDetails(ModelClassServiceImpl.class.getName(), classOrInterfaceDetails);
	}

	private boolean isEnumType(final JavaType javaType) {
		Validate.notNull(javaType, "Java type required");
		Validate.notNull(metadataService, "Metadata service required");
		ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService.getTypeDetails(javaType);
		if (javaTypeDetails != null) {
			if (javaTypeDetails.getPhysicalTypeCategory().equals(PhysicalTypeCategory.ENUMERATION)) {
				return true;
			}
		}
		return false;
	}

	private String getPlural(final JavaType javaType, final String metadataIdentificationString) {
		Validate.notNull(javaType, "Java type required");

		ClassOrInterfaceTypeDetails javaTypeDetails = typeLocationService.getTypeDetails(javaType);
		Validate.notNull(javaTypeDetails, "Class or interface type details isn't available for type '" + javaType + "'");
		LogicalPath logicalPath = PhysicalTypeIdentifier.getPath(javaTypeDetails.getDeclaredByMetadataId());
		String pluralMetadataKey = PluralMetadata.createIdentifier(javaType, logicalPath);
		PluralMetadata pluralMetadata = (PluralMetadata) metadataService.get(pluralMetadataKey);
		if (pluralMetadata != null) {
			registerDependency(pluralMetadata.getId(), metadataIdentificationString);
			String plural = pluralMetadata.getPlural();
			if (plural.equalsIgnoreCase(javaType.getSimpleTypeName())) {
				return plural + "Items";
			} else {
				return plural;
			}
		}
		return javaType.getSimpleTypeName() + "s";
	}

	private void registerDependency(String upstreamDependency, String downStreamDependency) {
		if (metadataDependencyRegistry != null
				&& StringUtils.isNotEmpty(upstreamDependency)
				&& StringUtils.isNotEmpty(downStreamDependency)
				&& !upstreamDependency.equals(downStreamDependency)
				&& !MetadataIdentificationUtils.getMetadataClass(downStreamDependency).equals(
						MetadataIdentificationUtils.getMetadataClass(upstreamDependency))) {
			metadataDependencyRegistry.registerDependency(upstreamDependency, downStreamDependency);
		}
	}

}
